package sanitycheck;

import java.util.ArrayList;
import java.util.HashSet;

import pipeline.EdgeDetection;
import setting.BioNLPSettings;
import def.AbstractText;
import def.ArgType;
import def.Argument;
import def.BioNLPDocument;
import def.Event;
import def.Protein;
import def.TSpan;
import exp.Count;

public class SolutionEnumeration {

	/**
	 * 
	 * @param doc
	 * @param triggers
	 */
	public static void enumerate(BioNLPDocument doc, int lineno, ArrayList<TSpan> triggers, EdgeDetection ed) {
		terminate = false;
		document = doc;
		truthEvents = new HashSet<Event>();
		text = (AbstractText) document.text;
		line = lineno;
		for (Event e:  doc.events.values()){
			if (text.getSentenceIdByPos(e.tspan.startIdx)!=line) continue;
			truthEvents.add(e);
		}
		if (truthEvents.size() == 0)
			return ;
//		truthEvents  = doc.events;
		
		ArrayList<TSpan> potentialParents = new ArrayList<TSpan>(), bindings = new ArrayList<TSpan>(), onlyChildren = new ArrayList<TSpan>();
		AbstractText text = (AbstractText)doc.text;
		for (TSpan trigger: triggers){
			if (text.getSentenceIdByPos(trigger.startIdx) != lineno) continue;
			if (ed.td.parentDictionary.contains(trigger.text.toLowerCase())){
				potentialParents.add(trigger);
			}		
			else{
				if (ed.td.bindingDictionary.contains(trigger.text.toLowerCase())){
					bindings.add(trigger);
				}
				else{
					onlyChildren.add(trigger);
				}
			}
		}
		ArrayList<Protein> proteins = new ArrayList<Protein>();
		for (int i = 0; i < doc.proteins.size(); i++){
			if (text.getSentenceIdByPos(doc.proteins.get(i).tspan.startIdx) != lineno) continue;
			proteins.add(doc.proteins.get(i));
		}

		// setting search depth: [ m1*(m+n) +m2*n ] * percentage
		maxLength = (int)( (potentialParents.size() * (triggers.size() +  proteins.size() +bindings.size()- 1)
				+(onlyChildren.size()+bindings.size()) 
				* proteins.size())*(BioNLPSettings.getFloatValue("maxPerc")-0.05*potentialParents.size()));
		
		
//		for (int i = 0; i < BioNLPSettings.DE; i ++){
//			for (int j = 0; j < BioNLPSettings.DP; j++){


				ArrayList<ArrayList<Event>> solutions = new ArrayList<ArrayList<Event>>();
				System.err.println("Generating events with "+potentialParents.size()+"  "+bindings.size()+"   "+onlyChildren.size()+" "+proteins.size());
				System.err.println("parents:: "+potentialParents);
				System.err.println("bindings::"+bindings);
				System.err.println("children::"+onlyChildren);
				System.err.println("proteins::"+proteins);
				generateEvents(potentialParents, bindings, onlyChildren, proteins, 0,0, 0, 0, new ArrayList<Event>(), solutions);

//			}
//		}


	}
	public static HashSet<Event>  truthEvents = null;
	public static AbstractText text = null;
	public static BioNLPDocument document = null;
	public static int maxLength = 0;
	public static int line = -1;
	public static int debugCounter = 0;
	public static boolean terminate = false;
	/**
	 * prepare the events from tspans, including selecting the tspans, duplicating the events
	 * @param parents
	 * @param bindings
	 * @param children
	 * @param proteins
	 * @param i
	 * @param j
	 * @param be
	 * @param bp
	 * @param events
	 * @param solutions
	 */
	public static void generateEvents(
			ArrayList<TSpan> parents, ArrayList<TSpan> bindings,ArrayList<TSpan> children, ArrayList<Protein> proteins,
			int i, int j, int de, int dp, ArrayList<Event> events,ArrayList<ArrayList<Event>> solutions ) {
		if (terminate)
			return;
		if (i < parents.size()){
			// 0 time			
			generateEvents(parents, bindings, children, proteins, i+1, j, de, dp, events, solutions);
			
			// 1 time
			Event e = new Event();
			e.tspan = parents.get(i);
			events.add(e);
			generateEvents(parents, bindings, children, proteins, i+1, j, de, dp, events, solutions);
			
			// 1+ times
			if (de < BioNLPSettings.getIntValue("DE")){
				for (int m = 1; m < BioNLPSettings.getIntValue("BE"); m++){
					Event e2 = new Event();
					e2.tspan = parents.get(i);
					events.add(e2);
					generateEvents(parents,  bindings, children, proteins, i+1, j, de+1, dp, events, solutions);
				}
			}
			if (de < BioNLPSettings.getIntValue("DE")){
				for (int m = 0; m < BioNLPSettings.getIntValue("BE"); m++){
					events.remove(events.size()-1);
				}
			}
			else{
				events.remove(events.size()-1);
			}
		}else{
			if (j < children.size()+bindings.size()){
				// 0 time			
				generateEvents(parents, bindings, children, proteins, i, j+1, de, dp, events, solutions);
				
				// 1 time
				Event e = new Event();
				if (j > bindings.size()-1){
					e.tspan = children.get(j-bindings.size());
				}
				else{
					e.tspan = bindings.get(j);
				}
				events.add(e);
				generateEvents(parents, bindings, children, proteins, i, j+1, de, dp, events, solutions);
				
				// 1+ times
				if (dp < BioNLPSettings.getIntValue("DP")){
					for (int m = 1; m < BioNLPSettings.getIntValue("DP"); m++){
						Event e2 = new Event();
						if (j > bindings.size()-1){
							e2.tspan = children.get(j-bindings.size());
						}
						else{
							e2.tspan = bindings.get(j);
						}
						events.add(e2);
						generateEvents(parents, bindings, children, proteins, i, j+1, de, dp+1, events, solutions);
					}
				}
				
				if (dp < BioNLPSettings.getIntValue("DP")){
					for (int m = 0; m < BioNLPSettings.getIntValue("BP"); m++){
						events.remove(events.size()-1);
					}
				}
				else{
					events.remove(events.size()-1);
				}
			}
			else{
//				TSpan ts = null;
//				int c = 0;
//				for (int m = 0; m < events.size(); m++){
//					if (events.get(m).tspan == ts){
//						c++;
//					}
//					else{
//						if (c!=0)
//							System.err.print(ts.text+":"+c+" ");
//						c = 1;
//					}
//					ts = events.get(m).tspan;
//				}
//				if (ts!=null){
//					System.err.print(ts.text+":"+c+" ");
//				}
//				System.err.println("[]");
//				debugCounter++;
				buildEvents(events, parents, bindings, proteins, 0, solutions);			
				if (terminate)
					return;
			}
		}
		return;
	}

	private static boolean checkLength(ArrayList<Event> events){
		int length = 0;
		for (Event e: events){
			length += e.args.size();
		}
		if (length > maxLength)
			return false;
		else 
			return true;
	}
	
	/**
	 * link all events in <code>events</code> to some arguments
	 * @param events
	 * @param parents
	 * @param i
	 */
	private static void buildEvents(ArrayList<Event> events, ArrayList<TSpan> parents, ArrayList<TSpan> bindings, ArrayList<Protein> proteins, int i, ArrayList<ArrayList<Event>> solutions) {
		if (terminate)
			return;
		if (i < events.size()){
			// check if events exceed maxLength
			if (!checkLength(events))
				return;
			
			Event e = events.get(i);
			// check initial state
			if (e.args.size()!=0)
				System.err.println("buildEvents@Solution Enumeration: initially args not empty!!");
			// enumerate all possible links
			if (parents.contains(e.tspan)){
				// 1. take event as the only arg
				for (int j = 0; j < events.size(); j++){
					if (i==j) continue;
					if (events.get(j).tspan.equals(e.tspan)) continue;
					e.args.add(events.get(j));
					e.types.add(ArgType.Theme);
					boolean valid = checkEventLoop(e, new HashSet<Event>());
					if (valid){
						for (int k = 0; k < i; k++){
							if (events.get(k).equals(e)){
								valid = false;
								break;
							}
						}
						if (valid)
							buildEvents(events, parents, bindings ,proteins, i+1, solutions);
					}
					e.args.remove(0);
					e.types.remove(0);
				}

				// 2. take two event/protein(s) as theme or cause
				for (int j = 0; j < events.size()+proteins.size(); j++){
					if (i==j) continue;

					Argument arg1all = null;
					if (j >= events.size())
						arg1all = proteins.get(j - events.size());
					else
						arg1all = events.get(j);
					if (arg1all.tspan.equals(e.tspan)) continue;
					for (int k = 0; k < events.size()+proteins.size(); k++){
						if (i==k || j ==k) continue;
						Argument arg2all = null;
						if (k >= events.size())
							arg2all = proteins.get(k - events.size());
						else
							arg2all = events.get(k);
						if (arg2all.tspan.equals(e.tspan)) continue;
						if (arg2all.tspan.equals(arg1all.tspan)) continue;
						e.args.add(arg1all);
						e.args.add(arg2all);
						e.types.add(ArgType.Theme);
						e.types.add(ArgType.Cause);
						boolean valid = checkEventLoop(e, new HashSet<Event>());
						if (valid){
							boolean newvalid = true;
							for (int l = 0; l < i; l++){
								if (events.get(l).equals(e)){
									newvalid = false;
									break;
								}
							}
							if (newvalid)
								buildEvents(events, parents, bindings,proteins, i+1, solutions);
						}
						e.types.clear();
						// swap arg types
						e.types.add(ArgType.Cause);
						e.types.add(ArgType.Theme);
						// no need to check loop
						if (valid){
							for (int l = 0; l < i; l++){
								if (events.get(l).equals(e)){
									valid = false;
									break;
								}
							}
							if (valid)
								buildEvents(events, parents, bindings,proteins, i+1, solutions);
						}
						e.types.clear();
						e.args.clear();
					}
				}
				// END: parent triggers
			}
			else if (bindings.contains(e.tspan)){
				// 3. 2^#PROTEIN
				linkEventProtein(events, parents, bindings, proteins, i, solutions);
			}
			else{
				// 0/1 protein
				for (int j = 0; j < proteins.size(); j++){
					e.args.add(proteins.get(j));
					e.types.add(ArgType.Theme);
					boolean valid = true;
					for (int k = 0; k < i; k++){
						if (events.get(k).equals(e)){
							valid = false;
							break;
						}
					}
					if (valid)
						buildEvents(events, parents, bindings ,proteins, i+1, solutions);
					e.args.remove(0);
					e.types.remove(0);
				}
				
			}
		}
		else{
			// necessary to check? might remove it  because I check at each step of adding
//			System.err.print("check");
			if(check(events)){
//				System.err.println(" success");
				debugCounter++;
//				System.err.println(events);
//				if (debugCounter >1000){
//					System.exit(-1);
//				}
				if (debugCounter%10000 == 0){
					System.err.print(debugCounter+"\n");
					System.err.println(events);
				}
				Count c =new Count();
				eval(events,  c);
				if ((double)c.num / c.all> 0.8 && c.num > 4*c.fp){
					System.err.println("[GOOD]\t"+c.num+"   "+c.all+" "+c.fp+"\t"+events);
				}
				if (c.num == c.all && c.fp == 0){
					System.err.println("[FIT]"+events);
				}
				solutions.add(events);
//				if (debugCounter >= 1000000){
//					terminate  = true;
//					return;
//				}
					
			}
			else{
//				System.err.println(" failure");
			}
		}
	}


	public static void linkEventProtein(ArrayList<Event> events,
			ArrayList<TSpan> parents, ArrayList<TSpan> bindings, ArrayList<Protein> proteins, int i,
			ArrayList<ArrayList<Event>> solutions) {
		int choose = (proteins.size() > BioNLPSettings.getIntValue("MAX_CHOOSE"))? BioNLPSettings.getIntValue("MAX_CHOOSE"): proteins.size() ;
		int[] pos = new int[choose];
		for (int j = 0; j < choose; j++){
			pos[j] = -1;
		}
		int l = 0;
		int size =  proteins.size();
		Event e = events.get(i);
		for (int j  = 0; j < size; j++){
			// link e to proteins[j] 
			e.args.add(proteins.get(j));
			e.types.add(ArgType.Theme);
			// put pos[l]=j for backtrace
			pos[l] = j;
			l++;
			// build
//			for (int k = 0; k < l; k++){
//				System.out.print(" "+pos[k]);
//				System.err.print(" "+pos[k]);
//			}
//			System.out.println();
//			System.err.println(":"+e.args.size());
			boolean valid = true;
			for (int k = 0; k < i; k++){
				if (events.get(k).equals(e)){
					valid = false;
					break;
				}
			}
			if (valid)
				buildEvents(events, parents, bindings, proteins, i+1, solutions);
			
			if (l ==choose){
				// unlink and continue
				e.args.remove(l-1);
				e.types.remove(l-1);
				// cancel the back trace
				pos[l-1] = -1;
				l--;
			}
			
			// reach the end			
			if (j == size-1){
				if (l-1 <0)
					continue;
				
				// unlink l
				e.args.remove(l-1);
				e.types.remove(l-1);
				
				//look at the pos[l]
				// if pos[l] == end, unlink l-1 and return to pos[l-1]
				if (pos[l-1] == size -1){
					// if l-1 < 0, do nothing to jump out of the loop because it's done
					if (l-2 > -1){
						e.args.remove(l-2);
						e.types.remove(l-2);
						j = pos[l-2];
						pos[l-1] = -1;
						pos[l-2] = -1;
						l-=2;
					}
				}
				else{
					j = pos[l-1];
					pos[l-1] = -1;
					l--;
				}
			}
		}
		e.args.clear();
		e.types.clear();
	}

	/**
	 * return false if events are invalid, e.g. containing loops
	 * @param events
	 * @return
	 */
	public static boolean check(ArrayList<Event> events){
		boolean valid = true;
		HashSet<Event> visited = new HashSet<Event>();
		for (Event e: events){
			if (visited.contains(e)){
				valid = false;
				break;
			}
			if (!checkEvent(e, new HashSet<Event>())){
				valid = false;
				break;
			}
			visited.add(e);
		}
		return valid;
	}

	/**
	 * return false if addon makes the events invalid
	 * @param events: must be valid
	 * @param addon
	 * @return
	 */
	public static boolean check(ArrayList<Event> events, Event addon){
		boolean valid = true;
		return valid;
	}


	public static boolean checkEvent(Event event, HashSet<Event> visited){
		//		System.err.println("checking if already checked "+e );
		boolean valid = true;
		for (Event e: visited){
			if (event == e){
				// containing loop
				return false;
			}
		}
		visited.add(event);
		boolean hasProteinAsTheme = false;
		if (event.args.size() == 0){
			valid = false;
		}
		else if (event.args.size() ==1 ){
			if (event.types.get(0) != ArgType.Theme)
				valid = false;
			else{
				valid = checkArg(event.args.get(0), visited);
			}
		}else if (event.args.size() == 2){
			if ((event.types.get(0) ==  ArgType.Theme && event.types.get(1) == ArgType.Cause)
					||(event.types.get(0) ==  ArgType.Cause && event.types.get(1) == ArgType.Theme)){
				if (event.args.get(1) instanceof Event){
					visited.add((Event)event.args.get(1));
				}
				if (!checkArg(event.args.get(0), visited)){
					valid = false;
				}
				if (event.args.get(1) instanceof Event){
					visited.remove((Event)event.args.get(1));
				}
				if (event.args.get(0) instanceof Event){
					visited.add((Event)event.args.get(0));
				}
				if (!checkArg(event.args.get(1), visited)){
					valid = false;
				}
				if (event.args.get(0) instanceof Event){
					visited.remove((Event)event.args.get(0));
				}	
			}
			else{
				valid = false;
			}
			/*
			ArrayList<Integer> toRemove = new ArrayList<Integer>();
			for (int i = 0; i < event.args.size(); i++){
				if (event.args.get(i) instanceof Event){
					boolean flag = checkEvent((Event)event.args.get(i), visited);
					if (!flag){
						valid = flag;
						break;
					}
				}
				else if (event.args.get(i) instanceof Protein){
					if (event.types.get(i)== ArgType.Theme){
						hasProteinAsTheme = true;
					}
				}
			}
			for (int i = 0; i < toRemove.size(); i++){
				event.args.remove(toRemove.get(toRemove.size()-1-i));
				event.types.remove(toRemove.get(toRemove.size()-1-i));
			}
			if (event.args.size() > 0)
				valid = true;
			 */
		}
		else{
			// should all be proteins
			boolean allProteins = true;
			for (int i = 0; i < event.args.size(); i++){
				Argument arg = event.args.get(i);
				if (arg instanceof Event){
					allProteins = false;
					break;
				}
				else{
					if (event.types.get(i)!=ArgType.Theme){
						// has to be all Themes
						allProteins = false;
						break;
					}
				}
			}
			if (!allProteins)
				valid = allProteins;
		}
		return valid;
	}

	public static boolean checkEventLoop(Event event, HashSet<Event> visited){
		//		System.err.println("checking if already checked "+e );
		boolean valid = true;
		for (Event e: visited){
			if (event == e){
				// containing loop
				return false;
			}
		}
		visited.add(event);
		boolean hasProteinAsTheme = false;
		if (event.args.size() ==1 ){
			if (event.types.get(0) != ArgType.Theme)
				valid = false;
			else{
				if (event.args.get(0) instanceof Event){
					valid = checkEventLoop((Event)event.args.get(0), visited);
				}
			}
		}else if (event.args.size() == 2){
			if ((event.types.get(0) ==  ArgType.Theme && event.types.get(1) == ArgType.Cause)
					||(event.types.get(0) ==  ArgType.Cause && event.types.get(1) == ArgType.Theme)){
				if (event.args.get(0) instanceof Event){
					if (event.args.get(1) instanceof Event){
						visited.add((Event)event.args.get(1));
					}
					if (!checkEventLoop((Event)event.args.get(0), visited)){
						valid = false;
					}
					if (event.args.get(1) instanceof Event){
						visited.remove((Event)event.args.get(1));
					}
				}
				if (valid){
					if (event.args.get(1) instanceof Event){
						if (event.args.get(0) instanceof Event){
							visited.add((Event)event.args.get(0));
						}
						if (!checkEventLoop((Event)event.args.get(1), visited)){
							valid = false;
						}
						if (event.args.get(0) instanceof Event){
							visited.remove((Event)event.args.get(0));
						}	
					}
				}
			}
			else{
				valid = false;
			}
		}
		return valid;
	}

	public static void eval(ArrayList<Event> finalEvents, Count c) {
		int fp = finalEvents.size();
		
		HashSet<Event> tpInPred = new HashSet<Event>(), tpInTruth = new HashSet<Event>();
//		HashSet<Event> truths = new HashSet<Event>();
		HashSet<String> truthString = new HashSet<String>();
		for (Event e:  document.events.values()){
			if (text.getSentenceIdByPos(e.tspan.startIdx)!=line) continue;
			String str = e.toString();
			str = str.substring(str.indexOf("\t"));
			if (!truthString.contains(str)){
//				truths.add(e);
				truthString.add(str);
			}
		}
		for (Event e:truthEvents){
			c.all++;
			Event found = null;
			for (Event pe: finalEvents){
				//				if (tpInPred.contains(pe)) continue;
				if (pe.tspan.matchTruth(e.tspan, document)){
//					System.err.println("[EVAL TRUTH]"+e);
//					System.err.println("[EVAL PRED]"+pe);
					// FIXME consider Site arguments TODO
					// NOTE: forget about the event types for now
					int match = 0;
					if (e.args.size() == pe.args.size()){
						for (int j = 0; j < e.args.size(); j++){
							for (int k = 0; k < pe.args.size(); k++){
								if (e.args.get(j).equals(pe.args.get(k)) && e.types.get(j) ==  pe.types.get(k)){
									match++;
								}
							}
						}
					}
					else{
						match = -1;
					}
//					System.err.println("[EVAL EQUAL] "+(match ==  e.args.size())+" " +  match);
					if (match ==  e.args.size()){
						if (found!=null){
//							System.err.println("already matched this "+e+" ---with--- "+found+"\n***but "+pe);
							break;
						}
						tpInPred.add(pe);
						tpInTruth.add(e);
						c.num ++;
						fp -- ;
						found =pe;
						//						break;
					}
				}
			}
		}
		c.fp += fp;
	}

	public static boolean checkArg(Argument arg, HashSet<Event> visited){
		if (arg instanceof Event){
			return checkEvent((Event)arg, visited);
		}
		else{
			return true;
		}
	}
}
